﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Net;
using System.Net.Sockets;
using System.ComponentModel;

namespace ProgramacionRedes {
    public delegate void CantidadClientes_Changed(object sender, EventArgs_CantidadClientes e);

    public class EventArgs_CantidadClientes : EventArgs {
        private int cantidad;

        public int Cantidad {
            get { return cantidad; }
            set { cantidad = value; }
        }

        public EventArgs_CantidadClientes() {
            cantidad = 0;
        }

        public EventArgs_CantidadClientes(int cantidad) {
            this.cantidad = cantidad;
        }
    }

    public delegate void Log_Changed(object sender, EventArgs_Log e);

    public class EventArgs_Log : EventArgs {
        private string texto;

        public string Texto {
            get { return texto; }
            set { texto = value; }
        }

        public EventArgs_Log() {
            texto = "";
        }

        public EventArgs_Log(string texto) {
            this.texto = texto;
        }
    }

    public class Servidor {
        public ManualResetEvent allDone = new ManualResetEvent(false);
        private bool salir = false;
        IPEndPoint epLocal;
        TcpListener serverSocket;
        List<Conexion> conexiones;
        Sistema sistema;

        string ip;
        int puerto;

        public event CantidadClientes_Changed evCantCliChanged = null;
        public event Log_Changed evLogChanged = null;

        private BackgroundWorker bw;

        public Servidor(Sistema sistema) {
            this.sistema = sistema;
            conexiones = new List<Conexion>();
            bw = new BackgroundWorker();
            bw.WorkerReportsProgress = false;
            bw.DoWork+=new DoWorkEventHandler(bw_DoWork);
        }

        public void Terminar() {
            Log("Señal de finalización");
            salir = true;
            allDone.Set();
        }

        public void Iniciar(string ip, int puerto) {
            Log("Iniciando servidor");
            this.ip = ip;
            this.puerto = puerto;
            salir = false;
            bw.RunWorkerAsync();
        }

        private void bw_DoWork(object sender, DoWorkEventArgs e) {
            epLocal = new IPEndPoint(IPAddress.Parse(ip), puerto);
            serverSocket = new TcpListener(epLocal);

            // el servidor recién inicia, indico que no hay clientes conectados
            lock (conexiones) {
                NotificarCambioCantidadUsuariosConectados();
            }

            serverSocket.Start();
            Log("Servidor iniciado en " + ip + ":" + puerto);

            while (!salir) {
                allDone.Reset();
                serverSocket.BeginAcceptTcpClient(new AsyncCallback(DoAcceptSocketCallback), serverSocket);
                allDone.WaitOne();
            }
            Log("Terminando ...");
            foreach (Conexion h in conexiones) {
                Log("Cerrando conexion");
                h.Terminar();
            }
            Log("Cerrando socket de escucha");
            serverSocket.Stop();
            Log("Servidor terminado");
        }

        public void DoAcceptSocketCallback(IAsyncResult ar) {
            TcpListener listener = (TcpListener)ar.AsyncState;

            try {
                TcpClient socketCliente = listener.EndAcceptTcpClient(ar);
            
                Log("Conexión aceptada");

                // se crea un objeto conexion por cada cliente aceptado
                Conexion conexion = new Conexion();
                conexion.evLogChanged += new Log_Changed(conexion_evLogChanged);
                conexion.Iniciar(socketCliente, this);
            } catch(Exception exc) {
                Log("Error: " + exc.Message);
            }
            allDone.Set();
        }

        void conexion_evLogChanged(object sender, EventArgs_Log e) {
            string nombre_usuario;
            if (((Conexion)sender).Usuario != null) {
                nombre_usuario = ((Conexion)sender).Usuario.Nombre;
            } else {
                nombre_usuario = "?";
            }
            Log(nombre_usuario + ":" + e.Texto);
        }

        // Método llamado por un cliente (Conexion) para agregarse en la lista de clientes de este servidor.
        // El usuario que inicia la conexión debe hacer login antes de llamar a este método
        public void AgregarConexion(Conexion conexion) {
            // bloqueo la lista de conexiones porque cada cliente llama este método desde su propio hilo
            lock (conexiones) {
                if (conexiones.Contains(conexion)) {
                    Log("Usuario ya conectado");
                    throw new Exception("Usuario ya conectado");
                } else {
                    conexiones.Add(conexion);
                    if (evCantCliChanged != null) {
                        // el servidor recién inicia, indico que no hay clientes conectados
                        evCantCliChanged(this, new EventArgs_CantidadClientes(conexiones.Count));
                    }
                }
            }
        }

        // Método llamado por un cliente (Conexion) cuando finaliza la conexión a efectos de quitarse de la lista de clientes de este servidor
        public void QuitarConexion(Conexion conexion) {
            // bloqueo la lista de conexiones porque cada cliente llama este método desde su propio hilo
            lock (conexiones) {
                if (!conexiones.Contains(conexion)) {
                    Log("Usuario no está conectado");
                    throw new Exception("Usuario no está conectado");
                } else {
                    conexiones.Remove(conexion);
                    NotificarCambioCantidadUsuariosConectados();
                }
            }
        }

        // Método llamado desde la interfaz gráfica para obtener la cantidad de clientes conectados en cualquier momento
        public int CantidadUsuariosConectados() {
            int resultado = 0;

            // bloqueo la lista de conexiones porque está siendo accedida desde muchos hilo
            lock (conexiones) {
                resultado = conexiones.Count;
            }

            return resultado;
        }

        // Importante: Se debe tener el bloqueo de la lista de clientes para llamar este método
        private void NotificarCambioCantidadUsuariosConectados() {
            if (evCantCliChanged != null) {
                // el servidor recién inicia, indico que no hay clientes conectados
                evCantCliChanged(this, new EventArgs_CantidadClientes(conexiones.Count));
            }
        }

        internal List<string> ObtenerUsuariosConectados() {
            List<string> resultado = new List<string>();
            foreach (Conexion con in conexiones) {
                resultado.Add(con.Usuario.Nombre);
            }
            return resultado;
        }

        public Usuario ObtnerUsuarioNoConectado(string nombre, string clave) {
            foreach (Conexion c in conexiones) {
                if (c.Usuario.Nombre == nombre) {
                    throw new Exception("Usuario ya conectado");
                }
            }
            return sistema.ObtnerUsuarioNoConectado(nombre, clave);
        }

        private void Log(string texto) {
            if (evLogChanged != null) {
                evLogChanged(this, new EventArgs_Log(texto));
            }
        }

        public Usuario ObtnerUsuario(string nombre) {
            foreach (Usuario u in sistema.Usuarios) {
                if (u.Nombre == nombre) {
                    return u;
                }
            }
            throw new Exception("No existe un usuario con ese nombre");
        }
    }
}
